<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>Baked Shadow Shader — Alien.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="../assets/css/style.css">

    <style>
        :root {
            --bg-color: #fff;
            --ui-color: #000;
            --ui-color-triplet: 0 0 0;
        }
    </style>

    <script type="module">
        import { BasicMaterial, BufferGeometryLoader, BufferGeometryLoaderThread, Color, ColorManagement, DoubleSide, Group, ImageBitmapLoaderThread, LinearSRGBColorSpace, Mesh, OrbitControls, PerspectiveCamera, Scene, ShadowTextureMaterial, TextureLoader, Vector2, WebGLRenderer, getFullscreenTriangle, ticker } from '../../build/alien.three.js';

        class Cube extends Group {
            constructor() {
                super();
            }

            async initGeometry() {
                const { loadBufferGeometry } = WorldController;

                const geometry = await loadBufferGeometry('cube.json');

                // Use second set of UVs for cubemap row layout
                // https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/Textures/Cubemaps/CreatingCubemaps/
                geometry.attributes.uv = geometry.attributes.uv1;

                this.geometry = geometry;
            }

            async initMaterial() {
                const { anisotropy, loadTexture } = WorldController;

                // const map = await loadTexture('cubemap.jpg');
                const map = await loadTexture('cube/cube_basecolor.jpg');
                map.anisotropy = anisotropy;

                this.material = new BasicMaterial({ map });
            }

            initMesh() {
                const mesh = new Mesh(this.geometry, this.material);
                this.add(mesh);
            }

            // Public methods

            ready = async () => {
                await Promise.all([
                    this.initGeometry(),
                    this.initMaterial()
                ]);

                this.initMesh();
            };
        }

        // Based on https://oframe.github.io/ogl/examples/?src=skinning.html by gordonnl

        class Floor extends Group {
            constructor() {
                super();
            }

            async initGeometry() {
                const { loadBufferGeometry } = WorldController;

                this.geometry = await loadBufferGeometry('floor.json');
            }

            async initMaterial() {
                const { anisotropy, loadTexture } = WorldController;

                // const map = await loadTexture('uv.jpg');
                const map = await loadTexture('cube/cube_shadow.jpg');
                map.anisotropy = anisotropy;

                this.material = new ShadowTextureMaterial({ map });
                this.material.side = DoubleSide;
            }

            initMesh() {
                const mesh = new Mesh(this.geometry, this.material);
                mesh.position.y = -0.54;
                this.add(mesh);
            }

            // Public methods

            ready = async () => {
                await Promise.all([
                    this.initGeometry(),
                    this.initMaterial()
                ]);

                this.initMesh();
            };
        }

        class SceneView extends Group {
            constructor() {
                super();

                this.visible = false;

                this.initViews();
            }

            initViews() {
                this.floor = new Floor();
                this.add(this.floor);

                this.cube = new Cube();
                this.add(this.cube);
            }

            // Public methods

            animateIn = () => {
                this.visible = true;
            };

            ready = () => Promise.all([
                this.floor.ready(),
                this.cube.ready()
            ]);
        }

        class RenderManager {
            static init(renderer, scene, camera) {
                this.renderer = renderer;
                this.scene = scene;
                this.camera = camera;
            }

            // Public methods

            static resize = (width, height, dpr) => {
                this.renderer.setPixelRatio(dpr);
                this.renderer.setSize(width, height);
            };

            static update = () => {
                this.renderer.render(this.scene, this.camera);
            };
        }

        class WorldController {
            static init() {
                this.initWorld();
                this.initLoaders();
                this.initControls();

                this.addListeners();
            }

            static initWorld() {
                this.renderer = new WebGLRenderer({
                    powerPreference: 'high-performance',
                    antialias: true
                });

                // Output canvas
                this.element = this.renderer.domElement;

                // Disable color management
                ColorManagement.enabled = false;
                this.renderer.outputColorSpace = LinearSRGBColorSpace;

                // 3D scene
                this.scene = new Scene();
                this.scene.background = new Color(0xffffff);
                this.camera = new PerspectiveCamera(30);
                this.camera.near = 0.5;
                this.camera.far = 40;
                this.camera.position.z = 8;
                this.camera.lookAt(this.scene.position);

                // Global geometries
                this.screenTriangle = getFullscreenTriangle();

                // Global uniforms
                this.resolution = { value: new Vector2() };
                this.texelSize = { value: new Vector2() };
                this.aspect = { value: 1 };
                this.time = { value: 0 };
                this.frame = { value: 0 };

                // Global settings
                this.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
            }

            static initLoaders() {
                this.textureLoader = new TextureLoader();
                this.textureLoader.setPath('../assets/textures/');

                this.bufferGeometryLoader = new BufferGeometryLoader();
                this.bufferGeometryLoader.setPath('../assets/geometry/');
            }

            static initControls() {
                this.controls = new OrbitControls(this.camera, this.renderer.domElement);
                this.controls.enableDamping = true;
                // this.controls.enableZoom = false;
            }

            static addListeners() {
                this.renderer.domElement.addEventListener('touchstart', this.onTouchStart);
            }

            // Event handlers

            static onTouchStart = e => {
                e.preventDefault();
            };

            // Public methods

            static resize = (width, height, dpr) => {
                this.camera.aspect = width / height;
                this.camera.updateProjectionMatrix();

                if (width < height) {
                    this.camera.position.z = 10;
                } else {
                    this.camera.position.z = 8;
                }

                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.resolution.value.set(width, height);
                this.texelSize.value.set(1 / width, 1 / height);
                this.aspect.value = width / height;
            };

            static update = (time, delta, frame) => {
                this.time.value = time;
                this.frame.value = frame;

                this.controls.update();
            };

            // Global handlers

            static getTexture = (path, callback) => this.textureLoader.load(path, callback);

            static loadTexture = path => this.textureLoader.loadAsync(path);

            static getBufferGeometry = (path, callback) => this.bufferGeometryLoader.load(path, callback);

            static loadBufferGeometry = path => this.bufferGeometryLoader.loadAsync(path);
        }

        class App {
            static async init() {
                this.initThread();
                this.initWorld();
                this.initViews();
                this.initControllers();

                this.addListeners();
                this.onResize();

                await this.view.ready();
                this.view.animateIn();
            }

            static initThread() {
                ImageBitmapLoaderThread.init();
                BufferGeometryLoaderThread.init();
            }

            static initWorld() {
                WorldController.init();
                document.body.appendChild(WorldController.element);
            }

            static initViews() {
                this.view = new SceneView();
                WorldController.scene.add(this.view);
            }

            static initControllers() {
                const { renderer, scene, camera } = WorldController;

                RenderManager.init(renderer, scene, camera);
            }

            static addListeners() {
                window.addEventListener('resize', this.onResize);
                ticker.add(this.onUpdate);
                ticker.start();
            }

            // Event handlers

            static onResize = () => {
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                const dpr = window.devicePixelRatio;

                WorldController.resize(width, height, dpr);
                RenderManager.resize(width, height, dpr);
            };

            static onUpdate = (time, delta, frame) => {
                WorldController.update(time, delta, frame);
                RenderManager.update(time, delta, frame);
            };
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
